--- 模块功能：modbus模块管理
-- @module dl645
-- @author zpw @ DevelopLink
-- @license MIT
-- @copyright DevelopLink
-- @release 2020.11.15
require "pm"
require "httpv2"
require "utils"
require "crypto"
require "bit"
module(..., package.seeall)

require "default"

local slen = string.len
local write = default.write
local mbuid = 3 -- 默认串口3
local mbcid = 1 -- 默认网络通道 1
local mbTimeout = 2000
local mblist = {}
local mbOrders = {}
local writelist = {}  -- 下行数据发送列表
local recvBuff = default.getRecvBuff
local pubBuff = {} 
local readlist = 0
local pubTopic
local subTopic
local pubHookFunc  
local subHookFunc

function setPubHook(func)
     pubHookFunc = func
end
function setSubHook(func)
     subHookFunc = func
end
function getMbcid()
    return mbcid
end
function getMblist()
    return mblist
end
function getMbOrders()
    return mbOrders
end
function getWritelist()
    return writelist
end
function getPubBuff()
    return pubBuff
end
function getPubTopic()
    return pubTopic
end
function getSubBuff()
    return subTopic
end
-- 串口数据接收的回调
local function read485(uid, idx)
    local s = table.concat(recvBuff()[idx])
    recvBuff()[idx] = {}
    local sslen = slen(s)
    if sslen < 6 then
        return
    end
    local id, code = string.byte(s:sub(1, 1)), string.byte(s:sub(2, 2))
    local len, data = string.byte(s:sub(3, 3)), s:sub(4, -3)
    if id and code  and len  then
        -- 计算校验值
        _, check = pack.unpack(s:sub(-2, -1), "H")
        crc = crypto.crc16("dl645", s:sub(1, -3))
        if check == crc then
            sys.publish("DL_RECV", id, len, data)
            return
        else
            log.warn("dl645 crc failed", check, crc)
        end
    else
        log.warn("dl645 error frame")
    end
    sys.publish("DL_RECV", false, false, false)
end

function floatFomate(num, n)
    if type(num) ~= "number" then
        return num
    end
    n = n or 2
    if num < 0 then
        return -(math.abs(num) - math.abs(num) % 0.1 ^ n)
    else
        return num - num % 0.1 ^ n
    end
end

-- modbus发送一次命令
local function sendCmd(buff)
    local mcmd =   buff
    write(mbuid, mcmd)
    result, id, len, data = sys.waitUntil("DL_RECV", mbTimeout or 1000)
    -- 返回数据
    if result and id and len then
        return id, data, len
    else
        log.warn("dl645 read timeout")
        return nil, nil, nil
    end
end

-- 读取一次命令
local function readWriteOnce(buff)
    -- 最多重试3次
    for i=1, 3, 1 do      
        addr, data, len =  sendCmd(buff)
        -- 超时返回nil
        if addr ~= nil then
            return addr, data, len
        end
        sys.wait(1000)
    end
    return nil
end



-- 计算读取的寄存器值
local function calcRegs(idx, id, data, len)
    local regs = {}
    local orders = mblist[idx][5]
    local addr =  mblist[idx][6]
    for i=1, #orders do
        local offset = (orders[i][2] - addr) * 2
        if  offset < 0 then
            return regs
        end
        local od1,od2,od3,od4 = offset+orders[i][5][1], offset+orders[i][5][2], offset+orders[i][5][3], offset+orders[i][5][4]
        local hexval, res

        if orders[i][3] == 2  then
            _, res = pack.unpack(data, orders[i][4], offset + 1)   
        elseif len > 3  then
            hexval = table.concat({string.sub(data, od1,  od1) , string.sub(data, od2,  od2) , string.sub(data, od3,  od3), string.sub(data, od4,  od4) })
            -- hexval = mdb.getSeq(data, offset, orders[i][5][1],orders[i][5][2],orders[i][5][3],orders[i][5][4])
            _, res = pack.unpack(hexval, orders[i][4])
            if string.sub(orders[i][4], 2,  2) == "f" then
                res = floatFomate(res, 3)
            end
        else
            log.error("dl645 calcRegs error len!")
        end
        regs[orders[i][1]] = res
    end 
    return regs
end

local function msgPubMsg(idx, cmds, msg, cid)
    if idx > 0 then
        -- single publish
        if cid > 0  then
            msgPubHook(idx, cid, cmds, msg)
        end
    else
        -- full publish
        local buff = {}
        for i=1, #msg do    
            table.merge(buff, msg[i])
        end 
        if cid > 0  then
            msgPubHook(idx, cid, cmds, buff)
        end
    end
end

-- remote msg publish hook
function msgPubHook(idx, cid, cmds, msg)
    local buff = msg
    if pubHookFunc then
        buff =  pubHookFunc(msg)
    else
        buff = json.encode(buff)
    end
    log.info("dl645 upload", buff)
    sys.publish("NET_SENT_RDY_" .. cid , buff)
end

local function msgSubHandle(uid, buff)
    if not buff then
        return 
    end
    -- input table
    for key, value in pairs(buff) do
        local c = mbOrders[key]
        if c ~= nil then
            local cmdbuff = pack.pack(c[4], value) 
            local od1,od2,od3,od4 = c[5][1], c[5][2], c[5][3], c[5][4]
            if c[3]==2 then  
                cmdbuff = pack.pack("bb>H", c[1], 0x06, c[2])..cmdbuff
            else 
                hexval = table.concat({string.sub(cmdbuff, od1,  od1) , string.sub(cmdbuff, od2,  od2) , string.sub(cmdbuff, od3,  od3), string.sub(cmdbuff, od4,  od4) })
                cmdbuff = pack.pack("bb>H>Hb", c[1], 0x10, c[2], c[3]/2, c[3])..hexval
            end
            
            log.info("dl645 sub2", cmdbuff:toHex())
            table.insert(writelist, cmdbuff)
         end
    end
end


-- remote msg subscribe hook
function msgSubHook(uid, data)
    -- input must be json {"a":1}
    local buff, res= json.decode(data)
    log.info("dl645 sub0", data)
    if subHookFunc then
        buff =  subHookFunc(buff, uid)
    end
    -- 
    if buff~=nil and res then
        msgSubHandle(uid, buff)
    end

end



-- modbus任务启动 cmds指令列表[delay(s), cmd], 返回超时秒, 上行回调，下行回调， 绑定网络通道, 串口通道
function modbusStart(timeout, cid, uid, cmds)
    log.info("启动modbus任务")
    log.info("配置modbus", timeout, cid, uid)

    if cmds == nil or #cmds < 1 then
        log.error("dl645 config empty!")
        return 
    end
    mbTimeout = (timeout or 2) * 1000  -- default 2s
    mbuid =  uid
    mbcid =  tonumber(cid)
    pubTopic = "MB_PUB_" .. uid
    sys.unsubscribe("UART_RECV_WAIT_" .. uid, default.read)
    sys.subscribe("UART_RECV_WAIT_" .. uid, read485)
    sys.unsubscribe("UART_SENT_RDY_" .. uid, default.write)
    sys.subscribe("UART_SENT_RDY_" .. uid, msgSubHook)
    sys.subscribe(pubTopic, msgPubMsg)
    
    -- 初始化软件定时器
    for i = 1, #cmds do
        log.info("cmds")
        if cmds[i][1] and tonumber(cmds[i][1]) then
            local delay = tonumber(cmds[i][1]) or 2 --最快2s
            local upNow = tonumber(cmds[i][2]) > 0 -- 立即上报
            -- {delay, sigle, id, code, addr, load, data}
            local orders = cmds[i][7]
            log.info("dl645 orders len",  #orders)
            cmdhex = "fefefefe68"
            local addr = cmds[i][5]:fromHex()
            local load = cmds[i][6]:fromHex()
            startAddr = addr
            devid = cmds[i][3]
            
            if slen(devid) == 6 and slen(startAddr) and cmds[i][4] then
                cmdhex = cmdhex:fromHex()..cmds[i][3]:fromHex()..pack.pack("bbbpp",0x68,cmds[i][4],slen(load), addr, load)
                -- check
                local check = 0
                for k=1, slen(cmdhex) do
                    check = check + string.byte(string.sub(cmdhex,k,k))             
                end
                check = check % 256
                cmdhex = cmdhex..pack.pack("bb", check, 0x16)
            else
                cmdhex = nil
            end


            if cmdhex~=nil  and orders and #orders > 0 then
                local orlist = {}
                for j = 1, #orders do
                    table.insert(orlist, {orders[j][1], tonumber(orders[j][2]),  (orders[j][3]=="h" or orders[j][3]=="H") and 2 or 4, ">"..orders[j][3],
                    {tonumber(string.sub(orders[j][4],1,1)),tonumber(string.sub(orders[j][4],2,2)),tonumber(string.sub(orders[j][4],3,3)),tonumber(string.sub(orders[j][4],4,4))}})
                    --
                    mbOrders[orders[j][1]] =  {devid, tonumber(orders[j][2]),  (orders[j][3]=="h" or orders[j][3]=="H") and 2 or 4, ">"..orders[j][3],
                    {tonumber(string.sub(orders[j][4],1,1)),tonumber(string.sub(orders[j][4],2,2)),tonumber(string.sub(orders[j][4],3,3)),tonumber(string.sub(orders[j][4],4,4))}}

                end
                table.insert(mblist, {delay * 200, cmds[i][3]:fromHex(), rtos.tick(), upNow, orlist, startAddr}) --按tick算
                log.info("dl645 mblist insert ", delay * 200, cmds[i][3], rtos.tick(), upNow, startAddr)
            end
        end
    end
    local overFlag = bit.lshift(1, #mblist) - 1 
    if not mblist or #mblist < 1 then
        log.warn("dl645 empty config!")
        return
    end
    while true do
        sys.wait(200) 
        for i = 1, #mblist do
            local tick = rtos.tick()
            if (tick - mblist[i][3]) > mblist[i][1] then
                local id, data, len = readWriteOnce(mblist[i][2])
                if id ~= nil then
                    mblist[i][3]  = rtos.tick()
                    res = calcRegs(i, id, data, len)
                    pubBuff[i] = res
                    readlist = bit.bor(readlist,  bit.lshift(1, i-1))
                    if mblist[i][4] then
                        sys.publish(pubTopic, i, mblist, res, mbcid)
                    else
                        if overFlag == readlist then
                            readlist = 0
                            sys.publish(pubTopic, -1, mblist, pubBuff, mbcid)
                        end
                    end
                else
                    log.warn("dl645 timeout:", mblist[i][2]:toHex())
                end
            end
        end

        if #writelist then
            -- write cmd
            for i = 1, #writelist, 1 do
                readWriteOnce(writelist[i])
            end
            writelist = {}
        end
    end
end





